package com.bytebuddytest.annotation;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import org.junit.Assert;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;

import static net.bytebuddy.matcher.ElementMatchers.named;

/**
 * @author yutianhong
 * @version 1.0
 * @since 2023/10/25 11:44
 */
@SuppressWarnings("resource")
public class AnnotationTest {
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RuntimeDefinition {
    }

    public static class RuntimeDefinitionImpl implements RuntimeDefinition {
        @Override
        public Class<? extends Annotation> annotationType() {
            return RuntimeDefinition.class;
        }
    }

    @RuntimeDefinition
    public static class Parent {
        @Override
        @RuntimeDefinition
        public String toString() {
            return "parent";
        }
    }

    @Test
    public void testAnnotationType() {
        DynamicType.Unloaded<Object> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .annotateType(new RuntimeDefinitionImpl())
                .make();
        // validate
        Class<?> dynamicClass = dynamicType
                .load(ClassLoader.getSystemClassLoader())
                .getLoaded();
        Annotation annotation = dynamicClass.getAnnotation(RuntimeDefinition.class);
        Assert.assertNotNull(annotation);
    }

    @Test
    public void testProxy() throws Exception {
        Class<Parent> sourceClass = Parent.class;
        Method sourceMethod = sourceClass.getDeclaredMethod("toString");
        DynamicType.Unloaded<Parent> dynamicType = new ByteBuddy()
                .subclass(sourceClass)
                .annotateType(sourceClass.getDeclaredAnnotations())
                .method(ElementMatchers.is(sourceMethod))
                .intercept(FixedValue.value("proxy"))
                .annotateMethod(sourceMethod.getDeclaredAnnotations())
                .make();

        save(dynamicType);

        Class<? extends Parent> dynamicClass = dynamicType.load(ClassLoader.getSystemClassLoader())
                .getLoaded();
        String string = dynamicClass.newInstance().toString();
        Assert.assertEquals(string, "proxy");

        RuntimeDefinition annotation = dynamicClass.getAnnotation(RuntimeDefinition.class);
        Assert.assertNotNull(annotation);
    }

    private void save(DynamicType.Unloaded<?> dynamicType) throws IOException {
        @SuppressWarnings("ConstantConditions")
        String folderPath = getClass().getClassLoader().getResource("").getPath() + "save";
        System.out.println("folderPath = " + folderPath);
        dynamicType.saveIn(new File(folderPath));
    }

    @Test
    public void testMethodAndField() throws Exception {
        DynamicType.Unloaded<Object> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .annotateType(new RuntimeDefinitionImpl())
                .method(named("toString"))
                .intercept(SuperMethodCall.INSTANCE)
                .annotateMethod(new RuntimeDefinitionImpl())
                .defineField("foo", Object.class)
                .annotateField(new RuntimeDefinitionImpl())
                .make();

        save(dynamicType);

        Class<?> dynamicClass = dynamicType.load(ClassLoader.getSystemClassLoader())
                .getLoaded();
        RuntimeDefinition methodAnnotation = dynamicClass.getDeclaredMethod("toString")
                .getAnnotation(RuntimeDefinition.class);
        Assert.assertNotNull(methodAnnotation);
        RuntimeDefinition fieldAnnotation = dynamicClass.getDeclaredField("foo")
                .getAnnotation(RuntimeDefinition.class);
        Assert.assertNotNull(fieldAnnotation);
    }

    // test annotation method
    // 只会被调用一次，还是只能存常量

    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        String uuid();
    }

    public static class MyAnnotationImpl implements MyAnnotation {
        public static int i = 0;
        @Override
        public String uuid() {
            System.out.println(this);
            if (i != 0) {
                throw new RuntimeException("unexpected");
            }
            return String.valueOf(i++);
        }

        @Override
        public Class<? extends Annotation> annotationType() {
            return MyAnnotation.class;
        }
    }

    @Test
    public void testComplexAnnotationMethod() throws Exception {
        DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
                .subclass(Object.class)
                .annotateType(new MyAnnotationImpl())
                .make();

        save(unloaded);
        Class<?> loadedClass = unloaded.load(ClassLoader.getSystemClassLoader())
                .getLoaded();
        MyAnnotation annotation = loadedClass.getAnnotation(MyAnnotation.class);
        annotation.uuid();
        annotation.uuid();
        Assert.assertEquals(annotation.uuid(), "0");
    }

}
